﻿using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using static System.Text.Encoding;
using System.IO;
using ICSharpCode.SharpZipLib.Zip;
using System.Net;
using ModernHttpClient;

namespace Moven.Data
{
	public class Article
	{
		public int Index { get; set; }
		public string Url { get; set; }
		public string Title { get; set; }
		public string[] Tags { get; set; }
		public string Date { get; set; }

		public string JoinedTags => string.Join(" ", Tags);
	}

	public class MovenArticles
	{
		public DateTime? UpdateTime { get; set; }
		public int MaxIndex { get; set; }
		public Article[] Articles { get; set; }
	}

	public class Tag
	{
		public string Name { get; set; }
		public int Count { get; set; }
	}

	public enum BookmarkAction
	{
		Add,
		Remove
	}

	public class BookmarkOperation
	{
		public DateTime Timestamp { get; set; } = DateTime.UtcNow;
		public BookmarkAction Action { get; set; }
		public int[] Indices { get; set; }
	}

	public enum RefreshArticlesStatus
	{
		Success,
		Failure
	}

	public class RefreshArticlesData
	{
		public RefreshArticlesStatus Status { get; set; }
		public IEnumerable<Article> NewArticles { get; set; } = Enumerable.Empty<Article>();
	}

	public enum SyncBookmarksStatus
	{
		Success,
		SuccessWithChanges,
		Failure
	}

	public class SyncBookmarksData
	{
		public SyncBookmarksStatus Status { get; set; }
		public IEnumerable<int> SyncedBookmarks { get; set; } = Enumerable.Empty<int>();
	}

	public class ArticlesStore
	{
		public const string DataUrlTemplate = "https://code.csdn.net/book_imgen/moven/blob/master/updates/{MaxIndex}/articles.txt",
			ArticlesKey = "MovenArticles", BookmarksKey = "Bookmarks", BookmarkOperationsKey = "BookmarkOperations";

		static readonly TimeSpan MinIntervalBetweenUpdates = TimeSpan.FromDays(3);

		public static bool IsArticlesRefreshTest = false;

		IKeyValueStorage LocalStorage { get; }
		IKeyValueStorage CloudStorage { get; }

		MovenArticles _movenArticles;

		public ArticlesStore(IKeyValueStorage localStorage, IKeyValueStorage cloudStorage)
		{
			LocalStorage = localStorage;
			CloudStorage = cloudStorage;
		}

		public async Task<MovenArticles> GetArticlesAsync()
		{
			if (_movenArticles == null)
			{
				_movenArticles = await LocalStorage.GetValueAsync<MovenArticles>(ArticlesKey).ConfigureAwait(false);
				var predefinedMovenArticles = LocalStorage.GetValueFromPredefinedData<MovenArticles>(ArticlesKey);
				if (predefinedMovenArticles.UpdateTime > _movenArticles.UpdateTime)
				{
					_movenArticles = predefinedMovenArticles;
					LocalStorage.SetValueAsync(ArticlesKey, _movenArticles);
				}
				_movenArticles.Articles = _movenArticles.Articles.OrderByDescending(x => x.Index).ToArray();
			}
			return _movenArticles;
		}

		public async Task<Tag[]> GetTagsAsync()
		{
			await GetArticlesAsync().ConfigureAwait(false);
			var allTags = _movenArticles.Articles.
				SelectMany(x => x.Tags ?? new string[0]).ToArray();
			var uniqueTags = allTags.GroupBy(x => x).
				Select(x => new Tag { Name = x.Key, Count = x.Count() }).
				OrderByDescending(x => x.Count).
				ToArray();
			return uniqueTags;
		}

		public async Task<List<int>> GetBookmarksAsync()
		{
			var bookmarks = await LocalStorage.GetValueAsync<List<int>>(BookmarksKey).ConfigureAwait(false);
			return bookmarks ?? new List<int>();
		}

		public async Task SaveBookmarksAsync(List<int> bookmarks)
		{
			await LocalStorage.SetValueAsync(BookmarksKey, bookmarks).ConfigureAwait(false);
		}

		public async Task AddBookmarkOperationAsync(BookmarkOperation operation)
		{
			var operations = await LocalStorage.GetValueAsync<List<BookmarkOperation>>(BookmarkOperationsKey).ConfigureAwait(false);
			operations = operations ?? new List<BookmarkOperation>();
			operations.Add(operation);
			LocalStorage.SetValueAsync(BookmarkOperationsKey, operations);
		}

		public async Task ClearBookmarkOperationsAsync()
		{
			await LocalStorage.SetValueAsync(BookmarkOperationsKey, new List<BookmarkOperation>()).ConfigureAwait(false);
		}

		public async Task<List<BookmarkOperation>> GetBookmarkOperationsAsync()
		{
			return await LocalStorage.GetValueAsync<List<BookmarkOperation>>(BookmarkOperationsKey).ConfigureAwait(false);
		}

		public event EventHandler<SyncBookmarksData> BookmarksSynced;

		SyncBookmarksData InvokeBookmarksSyncedHandler(SyncBookmarksStatus status, IEnumerable<int> syncedBookmarks = null)
		{
			var data = new SyncBookmarksData
			{
				Status = status,
				SyncedBookmarks = syncedBookmarks
			};
			BookmarksSynced?.Invoke(this, data);
			return data;
		}

		public async Task<SyncBookmarksData> SyncBookmarksAsync(string phoneNo)
		{
			Func<Exception, SyncBookmarksData> exceptionHandler = ex =>
			{
				Console.WriteLine("Oops, bookmarks sync encountered error: " + ex);
				return InvokeBookmarksSyncedHandler(SyncBookmarksStatus.Failure);
			};
			var localOperationsTask = GetBookmarkOperationsAsync();
			List<BookmarkOperation> cloudOperations;
			try
			{
				cloudOperations = await CloudStorage.GetValueAsync<List<BookmarkOperation>>(phoneNo).ConfigureAwait(false);
			}
			catch (Exception ex)
			{
				return exceptionHandler(ex);
			}
			var localOperations = await localOperationsTask;
			var localBookmarks = await GetBookmarksAsync().ConfigureAwait(false);
			if (cloudOperations.IsNullOrEmpty())
			{
				// We have no data from the server, so we just need to upload local data to server
				try
				{
					if (localBookmarks.IsNullOrEmpty())
					{
						ClearBookmarkOperationsAsync();
						return InvokeBookmarksSyncedHandler(SyncBookmarksStatus.Success);
					}
					if (!localOperations.IsNullOrEmpty() && !localBookmarks.IsNullOrEmpty())
					{
						await CloudStorage.SetValueAsync(phoneNo, localOperations).ConfigureAwait(false);
						ClearBookmarkOperationsAsync();
					}
					return InvokeBookmarksSyncedHandler(SyncBookmarksStatus.Success);
				}
				catch (Exception ex)
				{
					return exceptionHandler(ex);
				}
			}

			List<int> bookmarks = null;
			if (localOperations.IsNullOrEmpty())
			{
				// We have no data locally, so we just need to get bookmarks from server
				bookmarks = BuildBookmarksFromOperations(cloudOperations);
				if (IsBookmarksInSync(localBookmarks, bookmarks)) // Local data and cloud data are in sync, so just report success
				{
					return InvokeBookmarksSyncedHandler(SyncBookmarksStatus.Success);
				}
				SaveBookmarksAsync(bookmarks);
				return InvokeBookmarksSyncedHandler(SyncBookmarksStatus.SuccessWithChanges, bookmarks);
			}

			// Both local and server has bookmarks data, we need to do a merge
			var mergedOperations = cloudOperations.Concat(localOperations).OrderBy(x => x.Timestamp).ToList();
			bookmarks = BuildBookmarksFromOperations(mergedOperations, true);

			try
			{
				await CloudStorage.SetValueAsync(phoneNo, mergedOperations).ConfigureAwait(false);
				ClearBookmarkOperationsAsync();
				if (IsBookmarksInSync(localBookmarks, bookmarks))
				{
					return InvokeBookmarksSyncedHandler(SyncBookmarksStatus.Success);
				}
				SaveBookmarksAsync(bookmarks);
				return InvokeBookmarksSyncedHandler(SyncBookmarksStatus.SuccessWithChanges, bookmarks);
			}
			catch (Exception ex)
			{
				return exceptionHandler(ex);
			}
		}

		static bool IsBookmarksInSync(List<int> localBookmarks, List<int> cloudBookmarks)
		{
			return localBookmarks.Count == cloudBookmarks.Count &&
				   localBookmarks.OrderBy(x => x).SequenceEqual(cloudBookmarks.OrderBy(x => x));
		}

		static List<int> BuildBookmarksFromOperations(IEnumerable<BookmarkOperation> operations, bool isOrdered = false)
		{
			var bookmarks = new List<int>();
			if (!isOrdered)
			{
				operations = operations.OrderBy(x => x.Timestamp);
			}
			operations = operations.Distinct((x, y) => x.Timestamp == y.Timestamp);
			foreach (var operation in operations)
			{
				if (operation.Action == BookmarkAction.Add)
				{
					operation.Indices.Where(index => !bookmarks.Contains(index)).ForEach(bookmarks.Add);
				}
				else
				{
					operation.Indices.ForEach(index => bookmarks.Remove(index));
				}
			}

			return bookmarks;
		}

		public event EventHandler<RefreshArticlesData> ArticlesRefreshed;

		RefreshArticlesData InvokeArticlesRefreshedHandler(RefreshArticlesStatus status = RefreshArticlesStatus.Success,
															IEnumerable<Article> newArticles = null)
		{
			var data = new RefreshArticlesData
			{
				Status = status,
				NewArticles = newArticles ?? Enumerable.Empty<Article>()
			};
			try
			{
				ArticlesRefreshed?.Invoke(this, data);
			}
			catch (Exception ex)
			{
				Console.WriteLine("Event handler ArticlesRefreshed throws exception: " + ex);
			}
			return data;
		}

		DateTime? _lastRefreshTime;
		static readonly TimeSpan MinIntervalBetweenRefreshes = TimeSpan.FromSeconds(20);
		public async Task<RefreshArticlesData> RefreshArticlesAsync(bool force = false)
		{
			var oldArticles = await GetArticlesAsync().ConfigureAwait(false);

			if (force && IsArticlesRefreshTest)
			{
				IEnumerable<Article> newArticles = new[]{
					new Article{
						Date = "2016-10-03",
						Index = 1300,
						Title = "如何永生",
						Tags = new[] { "健康" },
						Url = "https://www.bing.com"
					},
					new Article
					{
						Date = "2016-10-05",
						Index = 1290,
						Title = "如何长生不老",
						Tags = new []{ "健康", "长生不老" },
						Url = "https://www.bing.com"
					},
					new Article
					{
						Date = "2016-10-04",
						Index = 1289,
						Title = "那些永生的动物",
						Tags = new []{ "健康", "长生不老" },
						Url = "https://www.bing.com"
					}
				};
				oldArticles.Articles = newArticles.Concat(oldArticles.Articles).ToArray();
				oldArticles.UpdateTime = DateTime.UtcNow;
				return InvokeArticlesRefreshedHandler(RefreshArticlesStatus.Success, newArticles);
			}

			if (force && _lastRefreshTime != null &&
				DateTime.UtcNow - _lastRefreshTime.Value < MinIntervalBetweenRefreshes)
			{
				return InvokeArticlesRefreshedHandler();
			}
			if (!force && oldArticles != null &&
				DateTime.UtcNow - oldArticles.UpdateTime < MinIntervalBetweenUpdates)
			{
				return InvokeArticlesRefreshedHandler();
			}
			try
			{
				_lastRefreshTime = DateTime.UtcNow;
				// TODO: On iOS NativeMessageHandler will cause uncatchable exception in some cases, investigate it later on
				using (var httpClient = new HttpClient(new NativeMessageHandler())
				{
					Timeout = TimeSpan.FromSeconds(10)
				})
				{
					var url = DataUrlTemplate.Replace("{MaxIndex}", oldArticles.MaxIndex.ToString());
					var response = await httpClient.GetAsync(url).ConfigureAwait(false);
					if (response.StatusCode != HttpStatusCode.OK)
					{
						return InvokeArticlesRefreshedHandler();
					}
					var bytes = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
					string json = null;
					using (var memoryStream = new MemoryStream(bytes))
					{
						using (var zipFile = new ZipFile(memoryStream))
						{
							var jsonFileEntry = zipFile.GetEntry("articles.json");
							using (var stream = zipFile.GetInputStream(jsonFileEntry))
							{
								using (var streamReader = new StreamReader(stream, UTF8))
								{
									json = streamReader.ReadToEnd();
								}
							}
						}
					}

					var newArticles = JsonConvert.DeserializeObject<MovenArticles>(json);
					_movenArticles.MaxIndex = newArticles.MaxIndex;
					_movenArticles.UpdateTime = newArticles.UpdateTime;
					var additionalArticles = newArticles.Articles.OrderByDescending(x => x.Index).ToArray();
					_movenArticles.Articles = additionalArticles.Concat(_movenArticles.Articles).ToArray();
					LocalStorage.SetValueAsync(ArticlesKey, _movenArticles);

					return InvokeArticlesRefreshedHandler(RefreshArticlesStatus.Success, additionalArticles);
				}
			}
			catch (Exception ex)
			{
				Console.WriteLine("Oops, articles refresh encountered error: " + ex);
				return InvokeArticlesRefreshedHandler(RefreshArticlesStatus.Failure);
			}
		}
	}
}
